/*
 * Copyright (C) 2007 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.dialectek.blackguard;

import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Rect;
import android.graphics.Paint.Style;
import android.graphics.drawable.Drawable;
import android.opengl.GLUtils;

import java.nio.FloatBuffer;
import java.util.ArrayList;
import javax.microedition.khronos.opengles.GL10;
import javax.microedition.khronos.opengles.GL11;
import javax.microedition.khronos.opengles.GL11Ext;

/**
 * An OpenGL text label maker.
 *
 *
 * OpenGL labels are implemented by creating a Bitmap, drawing all the labels
 * into the Bitmap, converting the Bitmap into an Alpha texture, and creating a
 * mesh for each label
 *
 * The benefits of this approach are that the labels are drawn using the high
 * quality anti-aliased font rasterizer, full character set support, and all the
 * text labels are stored on a single texture, which makes it faster to use.
 *
 * The drawbacks are that you can only have as many labels as will fit onto one
 * texture, and you have to recreate the whole texture if any label text
 * changes.
 *
 */
public class LabelMaker {
   /**
    * Create a label maker
    * or maximum compatibility with various OpenGL ES implementations,
    * the strike width and height must be powers of two,
    * We want the strike width to be at least as wide as the widest window.
    *
    * @param fullColor true if we want a full color backing store (4444),
    * otherwise we generate a grey L8 backing store.
    * @param strikeWidth width of strike
    * @param strikeHeight height of strike
    */
   public LabelMaker(boolean fullColor, int strikeWidth, int strikeHeight)
   {
      mFullColor    = fullColor;
      mStrikeWidth  = strikeWidth;
      mStrikeHeight = strikeHeight;
      mTexelWidth   = (float)(1.0 / mStrikeWidth);
      mTexelHeight  = (float)(1.0 / mStrikeHeight);
      mClearPaint   = new Paint();
      mClearPaint.setARGB(0, 0, 0, 0);
      mClearPaint.setStyle(Style.FILL);
      mState = STATE_NEW;
      int i = strikeWidth / MAX_TEXTURE_STRIKE_WIDTH;
      if (strikeWidth > (i * MAX_TEXTURE_STRIKE_WIDTH)) { i++; }
      mTextureIDs   = new int[i];
      mStrikeWidths = new int[i];
      for (int j = 0; j < i; j++)
      {
         if (j < (i - 1))
         {
            mStrikeWidths[j] = MAX_TEXTURE_STRIKE_WIDTH;
         }
         else
         {
            mStrikeWidths[j] = mStrikeWidth - (j * MAX_TEXTURE_STRIKE_WIDTH);
         }
      }
      mTextureLabelMapper = new int[i];
      mCurrentTexture     = 0;
   }


   /**
    * Call to initialize the class.
    * Call whenever the surface has been created.
    *
    * @param gl
    */
   public void initialize(GL10 gl)
   {
      mState = STATE_INITIALIZED;
      gl.glGenTextures(mTextureIDs.length, mTextureIDs, 0);

      for (int i = 0; i < mTextureIDs.length; i++)
      {
         gl.glBindTexture(GL10.GL_TEXTURE_2D, mTextureIDs[i]);

         // Use Nearest for performance.
         gl.glTexParameterf(GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_MIN_FILTER,
                            GL10.GL_NEAREST);
         gl.glTexParameterf(GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_MAG_FILTER,
                            GL10.GL_NEAREST);

         gl.glTexParameterf(GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_WRAP_S,
                            GL10.GL_CLAMP_TO_EDGE);
         gl.glTexParameterf(GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_WRAP_T,
                            GL10.GL_CLAMP_TO_EDGE);

         gl.glTexEnvf(GL10.GL_TEXTURE_ENV, GL10.GL_TEXTURE_ENV_MODE,
                      GL10.GL_REPLACE);
      }
   }


   /**
    * Call when the surface has been destroyed
    */
   public void shutdown(GL10 gl)
   {
      if (gl != null)
      {
         if (mState > STATE_NEW)
         {
            gl.glDeleteTextures(mTextureIDs.length, mTextureIDs, 0);
            mState = STATE_NEW;
         }
      }
   }


   /**
    * Call before adding labels. Clears out any existing labels.
    *
    * @param gl
    */
   public void beginAdding(GL10 gl)
   {
      checkState(STATE_INITIALIZED, STATE_ADDING);
      mLabels.clear();
      mU          = 0;
      mV          = 0;
      mLineHeight = 0;
      Bitmap.Config config = mFullColor ?
                             Bitmap.Config.ARGB_4444 : Bitmap.Config.ALPHA_8;
      mBitmap = Bitmap.createBitmap(mStrikeWidths[mCurrentTexture], mStrikeHeight, config);
      mCanvas = new Canvas(mBitmap);
      mBitmap.eraseColor(0);
   }


   /**
    * Call to add a label
    *
    * @param gl
    * @param text the text of the label
    * @param textPaint the paint of the label
    * @return the id of the label, used to measure and draw the label
    */
   public int add(GL10 gl, String text, Paint textPaint)
   {
      return(add(gl, null, text, textPaint));
   }


   /**
    * Call to add a label
    *
    * @param gl
    * @param text the text of the label
    * @param textPaint the paint of the label
    * @return the id of the label, used to measure and draw the label
    */
   public int add(GL10 gl, Drawable background, String text, Paint textPaint)
   {
      return(add(gl, background, text, textPaint, 0, 0));
   }


   /**
    * Call to add a label
    * @return the id of the label, used to measure and draw the label
    */
   public int add(GL10 gl, Drawable drawable, int minWidth, int minHeight)
   {
      return(add(gl, drawable, null, null, minWidth, minHeight));
   }


   /**
    * Call to add a label
    *
    * @param gl
    * @param text the text of the label
    * @param textPaint the paint of the label
    * @return the id of the label, used to measure and draw the label
    */
   public int add(GL10 gl, Drawable background, String text, Paint textPaint,
                  int minWidth, int minHeight)
   {
      checkState(STATE_ADDING, STATE_ADDING);
      boolean drawBackground = background != null;
      boolean drawText       = (text != null) && (textPaint != null);

      Rect padding = new Rect();
      if (drawBackground)
      {
         background.getPadding(padding);
         minWidth  = Math.max(minWidth, background.getMinimumWidth());
         minHeight = Math.max(minHeight, background.getMinimumHeight());
      }

      int ascent            = 0;
      int descent           = 0;
      int measuredTextWidth = 0;
      if (drawText)
      {
         // Paint.ascent is negative, so negate it.
         ascent            = (int)Math.ceil(-textPaint.ascent());
         descent           = (int)Math.ceil(textPaint.descent());
         measuredTextWidth = (int)Math.ceil(textPaint.measureText(text));
      }
      int textHeight = ascent + descent;
      int textWidth  = Math.min(mStrikeWidths[mCurrentTexture], measuredTextWidth);

      int padHeight           = padding.top + padding.bottom;
      int padWidth            = padding.left + padding.right;
      int height              = Math.max(minHeight, textHeight + padHeight);
      int width               = Math.max(minWidth, textWidth + padWidth);
      int effectiveTextHeight = height - padHeight;
      int effectiveTextWidth  = width - padWidth;

      int centerOffsetHeight = (effectiveTextHeight - textHeight) / 2;
      int centerOffsetWidth  = (effectiveTextWidth - textWidth) / 2;

      // Make changes to the local variables, only commit them
      // to the member variables after we've decided not to throw
      // any exceptions.

      int u          = mU;
      int v          = mV;
      int lineHeight = mLineHeight;

      if (width > mStrikeWidths[mCurrentTexture])
      {
         width = mStrikeWidths[mCurrentTexture];
      }

      // Is there room for this string on the current line?
      if (u + width > mStrikeWidths[mCurrentTexture])
      {
         // No room, go to the next line:
         u          = 0;
         v         += lineHeight;
         lineHeight = 0;
      }
      lineHeight = Math.max(lineHeight, height);
      if (v + lineHeight > mStrikeHeight)
      {
         if (mCurrentTexture < mTextureIDs.length - 1)
         {
            endAdding(gl);
            ArrayList<Label> labels = new ArrayList<Label>();
            for (int i = 0; i < mLabels.size(); i++)
            {
               labels.add(mLabels.get(i));
            }
            mCurrentTexture++;
            beginAdding(gl);
            mLabels = labels;
            return(add(gl, background, text, textPaint, minWidth, minHeight));
         }
         else
         {
            throw new IllegalArgumentException("Out of texture space.");
         }
      }

      int u2    = u + width;
      int vBase = v + ascent;
      int v2    = v + height;

      if (drawBackground)
      {
         background.setBounds(u, v, u + width, v + height);
         background.draw(mCanvas);
      }

      if (drawText)
      {
         mCanvas.drawText(text,
                          u + padding.left + centerOffsetWidth,
                          vBase + padding.top + centerOffsetHeight,
                          textPaint);
      }

      Grid grid = new Grid(2, 2);
      // Grid.set arguments: i, j, x, y, z, u, v

      float texU  = u * mTexelWidth;
      float texU2 = u2 * mTexelWidth;
      float texV  = 1.0f - v * mTexelHeight;
      float texV2 = 1.0f - v2 * mTexelHeight;

      grid.set(0, 0, 0.0f, 0.0f, 0.0f, texU, texV2);
      grid.set(1, 0, width, 0.0f, 0.0f, texU2, texV2);
      grid.set(0, 1, 0.0f, height, 0.0f, texU, texV);
      grid.set(1, 1, width, height, 0.0f, texU2, texV);

      // We know there's enough space, so update the member variables
      mU          = u + width;
      mV          = v;
      mLineHeight = lineHeight;
      mLabels.add(new Label(grid, width, height, ascent,
                            u, v + height, width, -height));
      int id = mLabels.size() - 1;
      mTextureLabelMapper[mCurrentTexture] = id;
      return(id);
   }


   /**
    * Call to end adding labels. Must be called before drawing starts.
    *
    * @param gl
    */
   public void endAdding(GL10 gl)
   {
      checkState(STATE_ADDING, STATE_INITIALIZED);
      gl.glBindTexture(GL10.GL_TEXTURE_2D, mTextureIDs[mCurrentTexture]);
      GLUtils.texImage2D(GL10.GL_TEXTURE_2D, 0, mBitmap, 0);
      // Reclaim storage used by bitmap and canvas.
      mBitmap.recycle();
      mBitmap = null;
      mCanvas = null;
   }


   /**
    * Get the width in pixels of a given label.
    *
    * @param labelID
    * @return the width in pixels
    */
   public float getWidth(int labelID)
   {
      return(mLabels.get(labelID).width);
   }


   /**
    * Get the height in pixels of a given label.
    *
    * @param labelID
    * @return the height in pixels
    */
   public float getHeight(int labelID)
   {
      return(mLabels.get(labelID).height);
   }


   /**
    * Get the baseline of a given label. That's how many pixels from the top of
    * the label to the text baseline. (This is equivalent to the negative of
    * the label's paint's ascent.)
    *
    * @param labelID
    * @return the baseline in pixels.
    */
   public float getBaseline(int labelID)
   {
      return(mLabels.get(labelID).baseline);
   }


   /**
    * Begin drawing labels. Sets the OpenGL state for rapid drawing.
    *
    * @param gl
    * @param viewWidth
    * @param viewHeight
    */
   public void beginDrawing(GL10 gl, float viewWidth, float viewHeight)
   {
      checkState(STATE_INITIALIZED, STATE_DRAWING);
      gl.glShadeModel(GL10.GL_FLAT);
      gl.glEnable(GL10.GL_BLEND);
      gl.glBlendFunc(GL10.GL_SRC_ALPHA, GL10.GL_ONE_MINUS_SRC_ALPHA);
      gl.glColor4x(0x10000, 0x10000, 0x10000, 0x10000);
      gl.glMatrixMode(GL10.GL_PROJECTION);
      gl.glPushMatrix();
      gl.glLoadIdentity();
      gl.glOrthof(0.0f, viewWidth, 0.0f, viewHeight, 0.0f, 1.0f);
      gl.glMatrixMode(GL10.GL_MODELVIEW);
      gl.glPushMatrix();
      gl.glLoadIdentity();
      // Magic offsets to promote consistent rasterization.
      gl.glTranslatef(0.375f, 0.375f, 0.0f);
   }


   /**
    * Draw a given label at a given x,y position, expressed in pixels, with the
    * lower-left-hand-corner of the view being (0,0).
    *
    * @param gl
    * @param x
    * @param y
    * @param labelID
    */
   public void draw(GL10 gl, float x, float y, int labelID)
   {
      checkState(STATE_DRAWING, STATE_DRAWING);
      getCurrentTexture(labelID);
      gl.glBindTexture(GL10.GL_TEXTURE_2D, mTextureIDs[mCurrentTexture]);
      Label label = mLabels.get(labelID);
      gl.glEnable(GL10.GL_TEXTURE_2D);
      ((GL11)gl).glTexParameteriv(GL10.GL_TEXTURE_2D,
                                  GL11Ext.GL_TEXTURE_CROP_RECT_OES, label.mCrop, 0);
      ((GL11Ext)gl).glDrawTexiOES((int)x, (int)y, 0,
                                  (int)label.width, (int)label.height);
   }


   /**
    * Draw an array of text.
    *
    * @param gl
    * @param labelIDs
    * @param x
    * @param y
    */
   public void drawText(GL10 gl, int[] labelIDs, int x, int y)
   {
      for (int i = 0; i < labelIDs.length; i++)
      {
         draw(gl, x, y, labelIDs[i]);
         x += getWidth(labelIDs[i]);
      }
   }


   /**
    * Ends the drawing and restores the OpenGL state.
    *
    * @param gl
    */
   public void endDrawing(GL10 gl)
   {
      checkState(STATE_DRAWING, STATE_INITIALIZED);
      gl.glDisable(GL10.GL_BLEND);
      gl.glMatrixMode(GL10.GL_PROJECTION);
      gl.glPopMatrix();
      gl.glMatrixMode(GL10.GL_MODELVIEW);
      gl.glPopMatrix();
   }


   /**
    * Bind label texture.
    *
    * @param gl
    * @param labelID
    */
   public void bind(GL10 gl, int labelID)
   {
      checkState(STATE_INITIALIZED, STATE_DRAWING);
      getCurrentTexture(labelID);
      gl.glBindTexture(GL10.GL_TEXTURE_2D, mTextureIDs[mCurrentTexture]);
      gl.glShadeModel(GL10.GL_FLAT);
      gl.glEnable(GL10.GL_BLEND);
      gl.glBlendFunc(GL10.GL_SRC_ALPHA, GL10.GL_ONE_MINUS_SRC_ALPHA);
      gl.glColor4x(0x10000, 0x10000, 0x10000, 0x10000);
      Label label = mLabels.get(labelID);
      gl.glEnable(GL10.GL_TEXTURE_2D);
      ((GL11)gl).glTexParameteriv(GL10.GL_TEXTURE_2D,
                                  GL11Ext.GL_TEXTURE_CROP_RECT_OES, label.mCrop, 0);
   }


   /**
    * Bind label texture.
    *
    * @param gl
    * @param labelID
    * @param texCoords
    */
   public void bind(GL10 gl, int labelID, FloatBuffer texCoords)
   {
      float[] quadTexCoords =
      {
         0.0f, 0.0f, 0.0f, 1.0f, 1.0f, 1.0f,
         1.0f, 1.0f, 1.0f, 0.0f, 0.0f, 0.0f
      };

      checkState(STATE_INITIALIZED, STATE_DRAWING);
      getCurrentTexture(labelID);
      gl.glBindTexture(GL10.GL_TEXTURE_2D, mTextureIDs[mCurrentTexture]);
      gl.glShadeModel(GL10.GL_FLAT);
      gl.glEnable(GL10.GL_BLEND);
      gl.glBlendFunc(GL10.GL_SRC_ALPHA, GL10.GL_ONE_MINUS_SRC_ALPHA);
      gl.glColor4x(0x10000, 0x10000, 0x10000, 0x10000);
      Label label = mLabels.get(labelID);
      gl.glEnable(GL10.GL_TEXTURE_2D);

      float x_min = (float)(label.mCrop[0]) / (float)mStrikeWidths[mCurrentTexture];
      float x_max = (float)(label.mCrop[0] + label.mCrop[2]) / (float)mStrikeWidths[mCurrentTexture];
      float y_min = (float)(label.mCrop[1]) / (float)mStrikeHeight;
      float y_max = (float)(label.mCrop[1] + label.mCrop[3]) / (float)mStrikeHeight;
      for (int i = 0; i < quadTexCoords.length; i++)
      {
         if ((i % 2) == 0)
         {
            if (quadTexCoords[i] == 0.0f)
            {
               quadTexCoords[i] = x_min;
            }
            else
            {
               quadTexCoords[i] = x_max;
            }
         }
         else
         {
            if (quadTexCoords[i] == 0.0f)
            {
               quadTexCoords[i] = y_min;
            }
            else
            {
               quadTexCoords[i] = y_max;
            }
         }
      }
      texCoords.put(quadTexCoords);
      texCoords.position(0);
   }


   /**
    * Ends the label binding.
    *
    * @param gl
    */
   public void endBind(GL10 gl)
   {
      checkState(STATE_DRAWING, STATE_INITIALIZED);
      gl.glDisable(GL10.GL_BLEND);
   }


   /**
    * Get current texture for given label.
    *
    * @param labelID
    */
   public void getCurrentTexture(int labelID)
   {
      mCurrentTexture = 0;
      for (int i = 0; i < mTextureLabelMapper.length; i++)
      {
         if (labelID <= mTextureLabelMapper[i])
         {
            mCurrentTexture = i;
            return;
         }
      }
   }


   private void checkState(int oldState, int newState)
   {
      if (mState != oldState)
      {
         throw new IllegalArgumentException("Can't call this method now.");
      }
      mState = newState;
   }


   private static class Label
   {
      public Label(Grid grid, float width, float height, float baseLine,
                   int cropU, int cropV, int cropW, int cropH)
      {
         this.grid     = grid;
         this.width    = width;
         this.height   = height;
         this.baseline = baseLine;
         int[] crop    = new int[4];
         crop[0]       = cropU;
         crop[1]       = cropV;
         crop[2]       = cropW;
         crop[3]       = cropH;
         mCrop         = crop;
      }


      public Grid  grid;
      public float width;
      public float height;
      public float baseline;
      public int[] mCrop;
   }

   private static final int MAX_TEXTURE_STRIKE_WIDTH = 1024;
   private int              mStrikeWidth;
   private int              mStrikeHeight;
   private boolean          mFullColor;
   private Bitmap           mBitmap;
   private Canvas           mCanvas;
   private Paint            mClearPaint;

   private int[] mTextureIDs;
   private int[] mStrikeWidths;
   private int[] mTextureLabelMapper;
   private int   mCurrentTexture;

   private float            mTexelWidth;  // Convert texel to U
   private float            mTexelHeight; // Convert texel to V
   private int              mU;
   private int              mV;
   private int              mLineHeight;
   private ArrayList<Label> mLabels = new ArrayList<Label>();

   private static final int STATE_NEW         = 0;
   private static final int STATE_INITIALIZED = 1;
   private static final int STATE_ADDING      = 2;
   private static final int STATE_DRAWING     = 3;
   private int              mState;
}
